iT邦幫忙

2023 iThome 鐵人賽

DAY 10
0

useState 在背後做了什麼

先讓我們從 useState 認識起,他背後的原理其實是利用 JS 的陣列解構:

const number = [1, 2];
const [one, two] = number;
console.log(one, two); // 印出 1 和 2

useState 會回傳一個陣列,在這個陣列裡包含兩個東西: State 和可以改變 State 的函式。當我們宣告陣列,對應到 useState 時,就賦予我們自己定義的變數新的意義。不只方便開發者使用,也能同時完成變數宣告和套用 React 設好的 useState 兩件事。

const [text, setText] = useState('input value');
// text 對應到 State
// setText 對應到 setState()

正確理解 React Immutable State 特性

前面聽起來很簡單對吧?但一開始學習 React 時, State 更新機制絕對是我碰到數一數二的大魔王。這必須「歸功」於當時的我 JS 基礎不夠穩,只覺得東西有跑出來就好,沒有仔細研究這個語法和那個語法差在哪。

在 React 的世界中,要判斷一個值是否更新,並不是用直接在舊值上更新的方式,而是產生一個新值後直接取代。這也是為什麼前面一再強調,更新值必須透過 setState() 來更新,而不是其他方式。

// 正確
setText('update input value');
// 錯誤
text = 'update input value';
this.text  = 'update input value';

我們可以實驗看看,把程式碼改成這樣:

function App() {
  let text = 'input value';

  return (
    <SafeAreaView>
      <View>
        <Text>Text: {text}</Text>
        <TextInput
          value={text}
          onChangeText={newText => {
            text = newText;
            console.log(text);
          }}
        />

https://ithelp.ithome.com.tw/upload/images/20230912/20129635ZB240DGU0m.png
https://ithelp.ithome.com.tw/upload/images/20230912/20129635wmvdzgTxiX.png

會發現,雖然 console.log() 會印出更新後的值,但是畫面並沒有被正確更新。其實這是 React 用來省效能的一種方式。因為如果要一一比對每個值是否改變,再去更新那些有改變的值,當值十分複雜時,要花很多效能處理。因此 React 索性不比對了,只要直接去更改原始記憶體空間的,一律視為沒更新。只有產生新值直接取代,會被判定需要更新畫面。

你可能會覺得這跟我有何關係,何必大費周章講這麼多,反正要乖乖透過 useState 初始化,並用裡頭設定的 setState 更新值就是了嘛!讓我們接著來看下面的例子:

function App() {
  const [textArr, setTextArr] = useState(['a', 'b', 'c']);

  return (
    <SafeAreaView>
      <View>
        {textArr.map(text => (
          <Text key={text}>Text: {text}</Text>
        ))}
        <TouchableOpacity
          onPress={() => {
            const newTextArr = textArr.push('d');
            setTextArr(newTextArr);
            console.log(textArr);
          }}>
          <Text>按按鈕會新增 d</Text>
        </TouchableOpacity>

我們把資料改為字串陣列,設定一個按鈕,當用戶按下後,會將 d 這個字 push 進陣列中,用 setState 更新,並重新渲染畫面讓畫面上從「 Text: a Text: b Text:c 」變為「 Text: a Text: b Text:c Text:d 」。

不過當我們按下按鈕會發現,再一次的, console.log() 有顯示更新值,畫面卻壞掉了。為什麼呢?
https://ithelp.ithome.com.tw/upload/images/20230912/20129635wa0hJ7G1JB.png
https://ithelp.ithome.com.tw/upload/images/20230912/201296352UZMsqImzx.png

因為 push 這個方法,會直接更動原陣列。我們應該使用 map 、 filter 等會產生新值的方式去更新,否則即使透過 setState 做最後的更新,一樣會因改到原始記憶體空間,讓 React 誤以為值沒被更改。


React 如何更新畫面

React 18 後的 Functional Component 還有一個特點,是當 State 更新,會整個 function 從頭跑過一次。並不是只去更改一兩個變動的部分,而是採取一率將對應區塊全數清除,用更新後的完整資料全部重繪。
https://ithelp.ithome.com.tw/upload/images/20230912/20129635yuRFtSDinY.png

當然,如果該 Functional Component 裡有不只一個 State ,他會等到整個 function 都跑完了,確定有哪些 State 更新了,才一次重跑,不然太耗效能。
https://ithelp.ithome.com.tw/upload/images/20230912/20129635FcJ6LTUCRw.png

不過這種一率全部清除,再重新渲染的方式,怎麼想都很容易導致畫面卡住。因此, React 並不是直接操作真實 DOM 元素,而是透過 Reconcilier 在 Virtual DOM 上產生、管理 React Elements ,再比較新舊 Virtual DOM ,將差異給 Renderer 更新真實 DOM 。這樣的好處除了節省效能,也因為處理 React Elements 和處理渲染畫面的部分分開,使 React 在操作 DOM 上保有彈性,搭配 React DOM 就能處理瀏覽器、搭配 React Native 就能處理App 。


上一篇
Day 09. 事件處理與 React State (含 JSX 統整)
下一篇
Day 11. 子元件與 Props
系列文
即使明天老闆突然叫你用 React Native 也可以跟他說好沒問題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言